import base64, cloudscraper, os, ruamel.yaml, requests
from lxml import html
from modules import util
from modules.poster import ImageData
from modules.util import Failed
from requests.exceptions import ConnectionError
from tenacity import retry, stop_after_attempt, wait_fixed
from urllib import parse

logger = util.logger

image_content_types = ["image/png", "image/jpeg", "image/webp"]

def get_header(headers, header, language):
    if headers:
        return headers
    else:
        if header and not language:
            language = "en-US,en;q=0.5"
        if language:
            return {
                "Accept-Language": "eng" if language == "default" else language,
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0"
            }


def quote(data):
    return parse.quote(str(data))


def quote_plus(data):
    return parse.quote_plus(str(data))


def parse_qs(data):
    return parse.parse_qs(data)


def urlparse(data):
    return parse.urlparse(str(data))

class Version:
    def __init__(self, version_string="Unknown", part_string=""):
        self.full = version_string.replace("develop", "build")
        version_parts = self.full.split("-build")
        self.main = version_parts[0]
        self.build = 0
        self.part = int(part_string) if part_string else 0
        if len(version_parts) > 1:
            self.build = int(version_parts[1])

    def __bool__(self):
        return self.full != "Unknown"

    def __repr__(self):
        return str(self)

    def __str__(self):
        return f"{self.full}.{self.part}" if self.part else self.full

class Requests:
    def __init__(self, local, part, env_branch, git_branch, verify_ssl=True):
        self.local = Version(local, part)
        self.env_branch = env_branch
        self.git_branch = git_branch
        self.image_content_types = ["image/png", "image/jpeg", "image/webp"]
        self._nightly = None
        self._develop = None
        self._master = None
        self._branch = None
        self._latest = None
        self._newest = None
        self.session = self.create_session()
        self.scraper = cloudscraper.create_scraper()
        self.global_ssl = verify_ssl
        if not self.global_ssl:
            self.no_verify_ssl()

    def create_session(self, verify_ssl=True):
        session = requests.Session()
        if not verify_ssl:
            self.no_verify_ssl(session)
        return session

    def no_verify_ssl(self, session=None):
        if session is None:
            session = self.session
        session.verify = False
        if session.verify is False:
            import urllib3
            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    def download_image(self, title, image_url, download_directory, session=None, is_poster=True, filename=None):
        response = self.get_image(image_url, session=session)
        new_image = os.path.join(download_directory, f"{filename}") if filename else download_directory
        if response.headers["Content-Type"] == "image/jpeg":
            new_image += ".jpg"
        elif response.headers["Content-Type"] == "image/webp":
            new_image += ".webp"
        else:
            new_image += ".png"
        with open(new_image, "wb") as handler:
            handler.write(response.content)
        return ImageData("asset_directory", new_image, prefix=f"{title}'s ", is_poster=is_poster, is_url=False)

    def file_yaml(self, path_to_file, check_empty=False, create=False, start_empty=False):
        return YAML(path=path_to_file, check_empty=check_empty, create=create, start_empty=start_empty)

    def get_yaml(self, url, headers=None, params=None, check_empty=False):
        response = self.get(url, headers=headers, params=params)
        if response.status_code == 401:
            raise Failed(f"URL Error: Unauthorized - {url}")
        if response.status_code == 404:
            raise Failed(f"URL Error: No file found at {url}")
        if response.status_code == 429:
            raise Failed(f"URL Error: Too many requests -  {url}")
        if response.status_code >= 400:
            raise Failed(f"URL Error: {response.status_code} on {url}")
        return YAML(input_data=response.content, check_empty=check_empty)

    def get_image(self, url, session=None):
        response = self.get(url, header=True) if session is None else session.get(url, headers=get_header(None, True, None))
        if response.status_code == 404:
            raise Failed(f"Image Error: Not Found on Image URL: {url}")
        if response.status_code >= 400:
            raise Failed(f"Image Error: {response.status_code} on Image URL: {url}")
        if "Content-Type" not in response.headers or response.headers["Content-Type"] not in self.image_content_types:
            raise Failed("Image Not PNG, JPG, or WEBP")
        return response

    def get_stream(self, url, location, info="Item"):
        with self.session.get(url, stream=True) as r:
            r.raise_for_status()
            total_length = r.headers.get('content-length')
            if total_length is not None:
                total_length = int(total_length)
            dl = 0
            with open(location, "wb") as f:
                for chunk in r.iter_content(chunk_size=8192):
                    dl += len(chunk)
                    f.write(chunk)
                    logger.ghost(f"Downloading {info}: {dl / total_length * 100:6.2f}%")
                logger.exorcise()

    def get_scrape_html(self, url):
        html.fromstring(self.scraper.get(url).content)

    def get_html(self, url, headers=None, params=None, header=None, language=None):
        return html.fromstring(self.get(url, headers=headers, params=params, header=header, language=language).content)

    def get_json(self, url, json=None, headers=None, params=None, header=None, language=None):
        response = self.get(url, json=json, headers=headers, params=params, header=header, language=language)
        try:
            return response.json()
        except ValueError:
            logger.error(str(response.content))
            raise

    @retry(stop=stop_after_attempt(6), wait=wait_fixed(10))
    def get(self, url, json=None, headers=None, params=None, header=None, language=None):
        return self.session.get(url, json=json, headers=get_header(headers, header, language), params=params)

    def get_image_encoded(self, url):
        return base64.b64encode(self.get(url).content).decode('utf-8')

    def post_html(self, url, data=None, json=None, headers=None, header=None, language=None):
        return html.fromstring(self.post(url, data=data, json=json, headers=headers, header=header, language=language).content)

    def post_json(self, url, data=None, json=None, headers=None, header=None, language=None):
        response = self.post(url, data=data, json=json, headers=headers, header=header, language=language)
        try:
            return response.json()
        except ValueError:
            logger.error(str(response.content))
            raise

    @retry(stop=stop_after_attempt(6), wait=wait_fixed(10))
    def post(self, url, data=None, json=None, headers=None, header=None, language=None):
        return self.session.post(url, data=data, json=json, headers=get_header(headers, header, language))

    def has_new_version(self):
        return self.local and self.latest and self.local.main != self.latest.main or (self.local.build and self.local.build < self.latest.build)

    @property
    def branch(self):
        if self._branch is None:
            if self.git_branch in ["develop", "nightly"]:
                self._branch = self.git_branch
            elif self.env_branch in ["develop", "nightly"]:
                self._branch = self.env_branch
            elif self.local.build > 0:
                if self.local.main != self.develop.main or self.local.build <= self.develop.build:
                    self._branch = "develop"
                else:
                    self._branch = "nightly"
            else:
                self._branch = "master"
        return self._branch

    @property
    def latest(self):
        if self._latest is None:
            if self.branch == "develop":
                self._latest = self.develop
            elif self.branch == "nightly":
                self._latest = self.nightly
            elif self.local.build > 0:
                if self.local.main != self.develop.main or self.develop.build >= self.local.build:
                    self._latest = self.develop
                self._latest = self.nightly
            else:
                self._latest = self.master
        return self._latest

    @property
    def newest(self):
        if self._newest is None:
            self._newest = self.latest if self.latest and (self.local.main != self.latest.main or (self.local.build and self.local.build < self.latest.build)) else None
        return self._newest

    @property
    def master(self):
        if self._master is None:
            self._master = self._version("master")
        return self._master

    @property
    def develop(self):
        if self._develop is None:
            self._develop = self._version("develop")
        return self._develop

    @property
    def nightly(self):
        if self._nightly is None:
            self._nightly = self._version("nightly")
        return self._nightly

    def _version(self, level):
        try:
            url = f"https://raw.githubusercontent.com/Kometa-Team/Kometa/{level}/VERSION"
            return Version(self.get(url).content.decode().strip())
        except ConnectionError:
            return Version()


class YAML:
    def __init__(self, path=None, input_data=None, check_empty=False, create=False, start_empty=False):
        self.path = path
        self.input_data = input_data
        self.yaml = ruamel.yaml.YAML()
        self.yaml.width = 100000
        self.yaml.indent(mapping=2, sequence=2)
        try:
            if input_data:
                self.data = self.yaml.load(input_data)
            else:
                if start_empty or (create and not os.path.exists(self.path)):
                    with open(self.path, 'w'):
                        pass
                    self.data = {}
                else:
                    with open(self.path, encoding="utf-8") as fp:
                        self.data = self.yaml.load(fp)
        except ruamel.yaml.error.YAMLError as e:
            if "found character '\\t' that cannot start any token" in e.problem:
                location = f"{e.args[3].name}; line {e.args[3].line + 1} column {e.args[3].column + 1}"
                e = f"Tabs are not allowed in YAML files; only spaces are allowed.\nfirst tab character found at:\n{location}"
            else:
                e = str(e).replace("\n", "\n      ")
            
            raise Failed(f"YAML Error: {e}")
        except Exception as e:
            raise Failed(f"YAML Error: {e}")
        if not self.data or not isinstance(self.data, dict):
            if check_empty:
                raise Failed("YAML Error: File is empty")
            self.data = {}

    def save(self):
        if self.path:
            with open(self.path, 'w', encoding="utf-8") as fp:
                self.yaml.dump(self.data, fp)
